Skip to content

Conversation

@taipaise
Copy link
Collaborator

@taipaise taipaise commented Nov 22, 2025

🌁 Background

  • 바로 직전 PR에 이어지는 PR입니다!!

👩‍💻 Contents

  • 제보상세 ViewModel을 재활용하여 제보 완료 화면에서 제보 내용을 불러오도록 함
  • 번거로우시겠지만, [Feat-T3-204] 제보하기 로직 구현, 제보 히스토리 로직 구현 #73제보하기 완료 이후 플로우 (논의 필요) 부분을 먼저 검토해주시면 감사하겠습니다.
  • 만약 해당 방향대로 진행한다면, 앞 pr 반영 이후 해당 pr 반영하도록 하겠습니다.

👩‍💻 2차 Contents (25.12.04)

  • 이미지 URL 딕셔너리 제거
  • 이미지 선택 시, 최초 1장만 반영되는 버그 수정
  • 이미지가 이미 최대로 선택된 경우, bottom sheet 대신 alert 띄우는 것으로 수정
  • 제보하기 History, 제보하기 Detail 날짜 형식 피그마와 통일
  • 제보 등록 뷰에서 키보드 내려가기 추가
  • 추천 루틴 탭의 플로팅 버튼에도 제보하기 진입점 추가

📝 Review Note

1. 제보하기 이미지 선택 수정

image

기존 ReportRegistrationViewController에서 이미지 선택 시,
itemProvider = results.first?.itemProvider만 확인하여 만약 3장의 이미지를 선택하였더라도 첫번째 선택한 이미지만 적용되었던 버그가 있었습니다 !!

따라서 Results를 for문으로 돌면서 확인하는 식으로 약간 수정했어유 !!!!


Simulator Screen Recording - iPhone 16 Pro - 2025-12-04 at 17 43 54

또한 기존에는 현재 선택된 이미지가 3장이여도 bottom sheet를 통해 이미지나 사진 촬영을 허락한 후 이미지 선택이 적용될 때 Alert를 띄워주는 형식으로 흘러갔는데
약간 유저 입장에서 어색할 수 있다고 판단되어 ViewModel에 selectedPhotoCountPublisher를 추가해

유저가 카메라 버튼을 눌렀을 때 selectedPhotoCountPublisher를 통해 이미 max 까지 선택되었다면 Alert 아니라면 Bottom Sheet를 보여주는 식으로 수정하였습니다 !!!

그 외에 수정사항들은 매우 미비함니다 ~~~ 땽큐 딩동 !



Summary by CodeRabbit

  • 개선사항

    • 신고 작성 화면의 실시간 유효성 검사와 사진 카운트 기반 동작이 강화되어 등록 버튼 상태가 즉시 반영됩니다.
    • 신고 완료 후 로딩화면이 등록 결과에 따라 상세 혹은 이전 화면으로 이동합니다.
    • 신고 상세 화면이 뷰모델 출력에 따라 제목·카테고리·위치·설명·사진을 동적으로 표시합니다.
    • 사진 업로드 흐름이 추가되어 제출 시 사진을 서버에 전송합니다.
  • 신규

    • 카메라·사진 아이콘 자산과 선택 항목 아이콘 표시가 추가되었습니다.
    • 신고 작성 시 글자 수 표시(최대 150자)와 다중 사진 선택/카메라 기능이 제공됩니다.
    • 파일 presigned URL 조회 및 S3 업로드 연동이 도입되어 이미지 업로드가 가능해졌습니다.
  • 리팩토링

    • 리포트 작성·전송 흐름이 분리·정리되어 파일 업로드와 보고서 등록이 명확히 분리되었습니다.
    • 도메인·리포지토리·유스케이스 인터페이스가 변경되어 보고서 생성 및 파일 처리 호출 방식이 개선되었습니다.

✏️ Tip: You can customize this high-level summary in your review settings.

@taipaise taipaise requested a review from choijungp November 22, 2025 11:29
@taipaise taipaise self-assigned this Nov 22, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 22, 2025

Walkthrough

리포트 등록·파일 업로드(사전 서명 URL) 경로가 도입되고, 도메인 모델·리포지토리·엔드포인트·UseCase·ViewModel과 네트워크 바디 처리(rawData/json)가 연쇄적으로 변경되어 리포트 작성·사진 업로드·조회 흐름이 재구성되었습니다.

Changes

Cohort / File(s) 변경 요약
리포트 UI 및 등록 흐름
Projects/Presentation/Sources/Report/View/...
ReportRegistrationViewController.swift (delegate, 검증/퍼블리셔, 사진 선택/카운트, UI/레이아웃 변경), ReportLoadingViewController.swift (등록 완료 델리게이트 처리), ReportCompleteViewController.swift (BaseViewController 상속, Combine 바인딩, photo UIImageView 반환 변경), ReportHistoryViewController.swift, ReportHistoryTableViewCell.swift
프레젠테이션 뷰모델
Projects/Presentation/Sources/Report/ViewModel/...
ReportRegistrationViewModel (selectedPhotoCount, isReportValid, reportRegistrationCompletePublisher, register/검증 로직), ReportDetailViewModel (date 포맷·thumbnail 바인딩), ReportHistoryViewModel (selectedProgress 상태 변경·progress 재계산)
파일 업로드 엔드포인트·DTO·리포지토리
Projects/DataSource/Sources/Endpoint/..., Projects/DataSource/Sources/DTO/..., Projects/DataSource/Sources/Repository/FileRepository.swift
새 엔드포인트 FilePresignedEndpoint.swift(presigned-urls POST, bodyType .rawData 사용), S3UploadEndpoint.swift(PUT rawData 업로드), DTO 추가(FilePresignedConditionDTO, FilePresignedDTO), FileRepository 구현(사전서명 URL 조회 및 S3 업로드)
리포트 엔드포인트·리포지토리·UseCase 변경
.../Endpoint/ReportEndpoint.swift, .../Repository/ReportRepository.swift, .../UseCase/Report/ReportUseCase.swift
ReportEndpointregister(report:) 추가(POST), ReportRepository API가 상세 파라미터 기반으로 변경(throws, Int? 반환), ReportUseCase에 FileRepository 주입 및 사진 업로드(사전서명 조회 → 업로드 → report 호출) 로직 추가
도메인 모델·프로토콜 변경
Projects/Domain/Sources/...
ReportEntity(id 옵셔널, thumbnailURL 추가, photoURLs 명칭 변경), ReportType rawValue 지정·CustomStringConvertible, ReportRepositoryProtocolReportUseCaseProtocol 시그니처 변경/추가, FileRepositoryProtocol 추가
네트워크/Endpoint 처리 변경
Projects/DataSource/Sources/Common/Enum/Endpoint.swift, Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift, Projects/DataSource/Sources/NetworkService/NetworkService.swift
EndpointbodyType/bodyData 추가(기본 .json/nil), Endpoint+에서 bodyType에 따라 JSON 파라미터 생성 또는 rawData를 httpBody에 직접 설정, NetworkServiceEmptyResponseDTO 특수 처리 추가
DTO·매핑 세부 변경
Projects/DataSource/Sources/DTO/...
ReportDTOCodable로 변경, reportStatus 옵셔널화, thumbnailURL 매핑 추가 및 toReportEntity 변환 로직 조정; 새 DTO들 추가(FilePresignedConditionDTO, FilePresignedDTO)
DI 등록 변경
Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift, Projects/Domain/Sources/DomainDependencyAssembler.swift
FileRepositoryProtocol 등록 추가 및 ReportUseCase 생성 시 fileRepository 주입
UI 컴포넌트·아이콘·애셋·프로토콜
Projects/Presentation/Sources/Common/..., Projects/Presentation/Resources/Images.xcassets/...
SelectableItemicon 추가(디폴트 nil), SelectableItemCell 레이아웃/아이콘 처리 업데이트, BitnagilIcon에 camera/photo 아이콘 추가, 이미지 자산 추가(카메라·포토)
공유 유틸·포맷 추가
Projects/Shared/Sources/Extension/Date+.swift
Date.DateType에 yearMonthDateWeekyearMonthDateWeek2 포맷 추가

Sequence Diagram(s)

sequenceDiagram
    participant UI as ReportRegistrationVC
    participant VM as ReportRegistrationVM
    participant UC as ReportUseCase
    participant FR as FileRepository
    participant RR as ReportRepository
    participant NS as NetworkService

    UI->>VM: register() (title, content, category, location, photos)
    VM->>UC: report(title, content, category, location, photos)
    UC->>FR: fetchPresignedURL(prefix, filenames)
    FR-->>UC: presignedDict (filename -> presignedURL)
    loop for each photo
        UC->>FR: uploadFile(url: presignedURL, data)
        FR->>NS: S3Endpoint.uploadImage (PUT rawData)
        NS-->>FR: EmptyResponseDTO
    end
    UC->>RR: report(..., photoURLs) (ReportEndpoint.register POST)
    RR->>NS: performRequest (ReportEndpoint.register)
    NS-->>RR: BaseResponse / id
    RR-->>UC: id
    UC-->>VM: reportId
    VM-->>UI: reportRegistrationCompletePublisher (reportId)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

  • 주의할 파일/영역:
    • ReportUseCase.swift (사진 파일명 생성, 순차적 업로드, 오류/예외 경로)
    • ReportRepository.swift / ReportRepositoryProtocol (throws/반환값 변경에 따른 호출부 영향)
    • NetworkService, Endpoint+.swift (rawData httpBody 처리 및 EmptyResponseDTO 예외 처리)
    • ReportEntity / ReportDTO 매핑 및 nullable id/thumbnailURL 동기화
    • DI 어셈블러 (FileRepository 주입 누락 여부)

Possibly related PRs

🐰 깡충깡충 달려와요, 새 기능을 보러
서명 URL 가져와 파일 쏙 올려요
리포트가 반짝, 로그는 남고요 ✨
토끼가 춤추며 당근 한 입 드려요
축하해요, 코드에 꽃이 피었어요

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 '제보 완료화면 ViewModel 추가'로 주요 변경사항인 ReportCompleteViewController의 ViewModel 기반 리팩토링을 정확히 반영하고 있습니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/report-registration-completion

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift (1)

261-270: 사진 썸네일 표현 품질을 위해 contentMode 설정을 고려해 보세요.

현재 UIImageView에 배경색·cornerRadius만 지정되어 있어 실제 이미지를 로드하면 비율이 깨질 수 있습니다. 썸네일 용도라면 scaleAspectFill + clipsToBounds(true) 조합이 일반적입니다 (masksToBounds = true는 이미 설정되어 있으니 contentMode만 추가해도 효과가 있을 것 같습니다).

private func makePhotoView() -> UIImageView {
    let photoView = UIImageView()
    photoView.backgroundColor = BitnagilColor.gray30
    photoView.layer.masksToBounds = true
    photoView.layer.cornerRadius = 6
+   photoView.contentMode = .scaleAspectFill
    photoView.snp.makeConstraints { make in
        make.size.equalTo(Layout.photoSize)
    }
    return photoView
}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da2ae77 and abe4b88.

📒 Files selected for processing (1)
  • Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift (5 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: choijungp
Repo: YAPP-Github/Bitnagil-iOS PR: 68
File: Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift:108-172
Timestamp: 2025-11-19T13:49:08.326Z
Learning: ReportCompleteViewController에서 backgroudView가 fomoImageView와 의도적으로 겹치도록 설계됨. 이를 위해 backgroudView의 top 제약을 fomoImageView.snp.top (또는 fomoImageView)을 기준으로 설정하여 오프셋을 적용함.
📚 Learning: 2025-11-19T13:49:08.326Z
Learnt from: choijungp
Repo: YAPP-Github/Bitnagil-iOS PR: 68
File: Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift:108-172
Timestamp: 2025-11-19T13:49:08.326Z
Learning: ReportCompleteViewController에서 backgroudView가 fomoImageView와 의도적으로 겹치도록 설계됨. 이를 위해 backgroudView의 top 제약을 fomoImageView.snp.top (또는 fomoImageView)을 기준으로 설정하여 오프셋을 적용함.

Applied to files:

  • Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift
🧬 Code graph analysis (1)
Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift (2)
Projects/Presentation/Sources/Report/View/ReportDetailViewController.swift (4)
  • viewDidLoad (71-74)
  • configureAttribute (76-95)
  • configureLayout (97-136)
  • bind (138-145)
Projects/Presentation/Sources/Common/DesignSystem/Font/BitnagilFont.swift (1)
  • attributedString (50-67)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (3)
Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift (3)

8-12: BaseViewController 제네릭 + DI 패턴 적용이 일관되고 적절합니다.

BaseViewController<ReportDetailViewModel> 상속과 init(viewModel:reportId:) DI 패턴, 그리고 cancellables 보관 방식이 ReportDetailViewController와 일관되어 구조적으로 잘 맞습니다. 스토리보드를 쓰지 않는 화면이므로 fatalError 기반 coder-init 처리도 지금 컨텍스트에서는 무리 없어 보입니다.

Also applies to: 69-80


89-119: 기존 UI 속성 설정을 유지하면서 BaseViewController 패턴에 맞게 잘 정리되어 있습니다.

configureAttribute()를 override로 전환하면서 배경색, 폰트, 색상, 스택뷰 속성 등이 이전 구현 그대로 유지되고 있어, 화면 전반의 스타일링이 BaseViewController 라이프사이클과 자연스럽게 녹아듭니다.


121-185: 레이아웃 구성 및 fomo/background 오버랩 구조가 의도와 잘 맞습니다.

  • scrollViewcontentViewcontentLayoutGuide/frameLayoutGuide로 잡은 구조가 스크롤·오토레이아웃 관점에서 정석적인 패턴입니다.
  • Line 168–171에서 backgroudView.topfomoImageView(top) 기준으로 두고 오프셋을 주는 방식은, 이전에 공유된 “fomoImageView와 backgroundView가 겹치는 연출” 의도와도 부합해 보입니다.

Based on learnings

Comment on lines 82 to 87
override func viewDidLoad() {
super.viewDidLoad()
configureAttribute()
configureLayout()
fetchReport()
viewModel.action(input: .fetchReportDetail(reportId: reportId))
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

configureAttribute/configureLayout가 두 번 호출될 가능성이 있습니다.

BaseViewControllerviewDidLoad()에서 이미 configureAttribute(), configureLayout(), bind()를 호출하는 패턴이라면, Line 84–85에서 직접 다시 호출하면서 서브뷰 중복 추가·제약 중복 설정이 발생할 수 있습니다. 실제로 레이아웃 쪽은 두 번 도는 순간 Auto Layout 경고나 예기치 않은 뷰 계층 구조가 생길 수 있습니다.

이 컨트롤러도 ReportDetailViewController와 동일하게 super.viewDidLoad() 이후에는 액션만 호출하도록 정리하는 편이 안전해 보입니다.

 override func viewDidLoad() {
     super.viewDidLoad()
-    configureAttribute()
-    configureLayout()
     viewModel.action(input: .fetchReportDetail(reportId: reportId))
 }
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift
around lines 82–87, remove the duplicate calls to configureAttribute() and
configureLayout() in viewDidLoad() because BaseViewController already invokes
those (and bind()), which can cause duplicate subview additions and constraint
warnings; keep super.viewDidLoad() and only call the report fetch action
(viewModel.action(input: .fetchReportDetail(reportId: reportId))), or
alternatively guard against double-initialization if BaseViewController behavior
changes.

@taipaise
Copy link
Collaborator Author

taipaise commented Nov 22, 2025

CI 과정 발생한 오류는, 앞 PR 머지 이후 rebase 하면서 같이 작업하는게 좋을 것 같습니다~

taipaise and others added 4 commits November 23, 2025 14:35
* feat: NetworkService 로직 수정

- request 생성로직 수정. body에 jsonData 뿐 아니라, rawdata도 들어갈 수 있도록 수정 (s3 업로드 위함)
- response body가 empty여도 error를 throw 하지 않도록 수정

* feat: 제보하기 로직 구현

* refactor: 제보하기 수정된 디자인 적용

* feat: 제보하기 필터링 로직 구현

* feat: 제보히스토리 -> 제보상세 플로우 미비사항 구현

- 진행상황 collectionView cell에 진행상황 별 갯수 표시
- ReportEntity id 값을 옵셔널로 변경
- ReportDetail 날짜 포멧 변경

* fix: 제보하기 로직 수정

* refactor: 코드래빗 리뷰 반영
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Projects/Presentation/Sources/Common/Component/SelectableItemCell.swift (1)

75-102: 셀 재사용 시 아이콘 상태 및 displayName 조건 때문에 아이콘이 잘못 보일 수 있습니다

현재 configureCell 구현에는 두 가지 잠재적인 문제가 있습니다.

  1. displayName == nil 인 항목은 아이콘 로직을 타지 않습니다

    • guard let displayName = item.displayName else { ... return } 이후에만
      if let icon = item.icon {
          iconImageView.image = icon
          iconImageView.isHidden = false
      }
      가 호출됩니다.
    • SelectPhotoTypedisplayName == nil, icon 은 non-nil 이므로, 아이콘을 구현했음에도 실제로는 한 번도 보이지 않게 됩니다.
  2. 셀 재사용 시 이전 아이콘이 남을 수 있습니다

    • 어떤 아이템에서 icon 이 설정되어 iconImageView.isHidden = false 가 된 뒤,
      다음에 icon == nil 인 아이템으로 재사용되면, else 쪽에서 isHidden/image 를 리셋하지 않아 이전 아이콘이 그대로 남을 수 있습니다.
    • displayName == nil 경로에서는 아예 아이콘에 손대지 않고 곧바로 return 하기 때문에, 여기서도 같은 문제가 발생할 수 있습니다.

두 문제를 함께 해결하려면, 아이콘 설정/리셋을 guard 앞에서 공통으로 처리하는 것이 좋습니다. 예시는 다음과 같습니다.

 func configureCell(item: SelectableItem, isSelected: Bool) {
     checkIcon.isHidden = !isSelected
 
-    guard let displayName = item.displayName else {
-        titleLabel.text = item.description
-        return
-    }
-
-    if let icon = item.icon {
-        iconImageView.image = icon
-        iconImageView.isHidden = false
-    }
+    // 아이콘은 displayName 여부와 상관없이 항상 갱신
+    let icon = item.icon
+    iconImageView.image = icon
+    iconImageView.isHidden = (icon == nil)
+
+    guard let displayName = item.displayName else {
+        titleLabel.attributedText = nil
+        titleLabel.text = item.description
+        return
+    }
 
     let attributedString = NSMutableAttributedString(string: item.description)
     attributedString.addAttribute(
         .font,
         value: BitnagilFont(style: .body1, weight: .regular).font,
         range: NSRange(location: 0, length: item.description.count))
@@
-    titleLabel.attributedText = attributedString
+    titleLabel.attributedText = attributedString
 }

이렇게 정리하면

  • displayName 이 없는 항목(예: SelectPhotoType)도 아이콘은 정상 노출되고,
  • 셀 재사용 시에도 항상 현재 item.icon 기준으로 이미지/hidden 상태가 초기화되어 잘못된 아이콘이 남지 않게 됩니다.
Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift (1)

116-125: 등록 버튼 탭 시 로딩 화면/델리게이트 흐름은 좋지만, 한 가지 컴파일 이슈와 중복 실행 가능성이 있습니다

  1. reportRegistrationCompletePublisher 바인딩에서 delegate 앞에 self.가 빠져 있어 컴파일이 안 될 것 같습니다.
        viewModel.output.reportRegistrationCompletePublisher
            .receive(on: DispatchQueue.main)
            .sink { [weak self] reportId in
                guard let self else { return }

-                delegate?.reportRegistrationViewController(self, completeRegistration: reportId)
+                self.delegate?.reportRegistrationViewController(self, completeRegistration: reportId)
            }
            .store(in: &cancellables)
  1. 현재 구현에서는 등록 버튼을 여러 번 빠르게 탭하면 ReportLoadingViewController()가 그만큼 push되고, register()도 중복 호출될 수 있습니다. 실제 네트워크/서버 상태에 따라 중복 제보가 생길 여지가 있어서,
    • 첫 탭 이후에는 버튼 상태를 .inProgress 혹은 .disabled로 바꾸고,
    • 등록 완료/실패 시 다시 활성화
      하는 식으로 in‑flight 상태를 ViewModel에서 관리해 주면 더 안전할 것 같습니다.

Also applies to: 407-425

♻️ Duplicate comments (1)
Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift (1)

83-88: configureAttribute/configureLayoutviewDidLoad에서 다시 호출하면 중복 초기화가 발생할 수 있습니다

BaseViewControllerviewDidLoad()에서 이미 configureAttribute(), configureLayout(), bind()를 호출하는 패턴이라면, 여기서 다시 직접 호출하면 서브뷰 추가·제약 설정이 두 번씩 일어나 Auto Layout 경고나 예기치 않은 UI 결과가 나올 수 있습니다 (이전에 같은 이슈가 한 번 지적된 적 있습니다).

이 컨트롤러도 ReportDetailViewController와 동일하게 super.viewDidLoad() 이후에는 액션만 호출하도록 정리하는 것이 안전해 보입니다.

    override func viewDidLoad() {
        super.viewDidLoad()
-        configureAttribute()
-        configureLayout()
        viewModel.action(input: .fetchReportDetail(reportId: reportId))
    }
🧹 Nitpick comments (17)
Projects/Presentation/Sources/Report/Model/SelectPhotoType.swift (1)

8-9: 카메라/앨범 타입별 아이콘 매핑 무난합니다

SelectableItem.icon 요구사항에 맞게 BitnagilIcon 을 사용해 명확히 매핑되어 있고, 옵셔널 처리도 자연스럽습니다.
템플릿 컬러로 틴팅해야 하는 디자인이라면, 에셋 쪽에서 withRenderingMode(.alwaysTemplate) 적용 여부만 한 번 확인해 두면 좋겠습니다.

Also applies to: 38-45

Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)

79-80: 리포트용 카메라/사진 아이콘 추가 이상 없습니다

새 에셋 이름(bitnagil_camera_icon, bitnagil_photo_icon)과 SelectPhotoType.icon 사용처가 일관되고, 다른 아이콘 정의 방식과도 형식이 동일해서 유지보수에 문제 없어 보입니다.
필요하다면 이 아이콘도 다른 곳처럼 템플릿 렌더링(withRenderingMode(.alwaysTemplate))을 사용할지 디자인 가이드에 맞춰 통일만 고민해 보시면 될 것 같습니다.

Projects/Domain/Sources/Protocol/Repository/FileRepositoryProtocol.swift (1)

1-23: Presigned URL 반환 타입의 옵셔널 여부 한 번만 정리해 보면 좋겠습니다

fetchPresignedURL[String: String]? 를 반환하도록 설계되어 있는데, FileRepository 구현에서는 현재 항상 non-optional 딕셔너리를 리턴하고 있어 실제로 nil 이 나올 상황이 거의 없어 보입니다.

  • presigned URL 발급이 실패하면 throw 로 처리하고
  • 성공 시에는 항상 모든 파일 이름에 대한 URL 을 받는 구조라면

반환 타입을 [String: String] 로 단순화해도 좋을 것 같습니다. 반대로, 일부 케이스에서 응답 바디가 없거나 비어 있을 수 있다면, 그 시나리오를 주석에 조금 더 명시해 두면 호출 측에서 이해하기 쉬울 것 같습니다.

Projects/Presentation/Sources/Common/Component/SelectableItemCell.swift (1)

14-18: 스택뷰 기반 레이아웃 전환은 괜찮습니다 (axis/size 세부 설정만 참고)

stackView 로 아이콘 + 타이틀을 묶고, 고정 높이/마진을 주는 구조는 명확하고 유지보수도 좋아 보입니다.
소소하게 고려해 볼 만한 부분은 아래 정도입니다.

  • stackView.axis 는 기본값이 .horizontal 이라 현재도 동작에는 문제 없지만, 명시적으로 .horizontal 을 지정해 두면 의미가 더 분명해집니다.
  • iconImageView 는 크기를 20pt 정사각형으로 고정해 두셨는데, 아이콘 에셋 비율에 따라 간혹 찌그러질 수 있어 contentMode = .scaleAspectFit 정도를 설정해 두면 안전합니다.
  • stackViewwidth == 0 (priority .medium) 제약을 주신 부분은 레이아웃 경고 방지용 의도로 보이는데, 나중에 디버깅할 때 혼란을 줄 수 있으니, 실제로 필요한지 한 번 더 확인해 보시고 필요 없다면 제거하는 것도 옵션입니다.

전반적으로 기능적인 문제는 없어 보이고, 위 내용은 선택적인 정리 사항에 가깝습니다.

Also applies to: 20-22, 35-49, 51-66

Projects/DataSource/Sources/Repository/ReportRepository.swift (1)

14-38: 신규 report 구현은 일관적이지만, nil id 의미를 명확히 해두는 편이 좋습니다

ReportDTO 구성과 ReportEndpoint.register 호출 흐름은 ReportUseCase.report와 잘 맞습니다. 다만 networkService.request 결과가 nil인 경우를 그대로 nil id로 올려보내고 있어서, 상위 레이어에서 “요청 실패(에러)”와 “정상 응답이지만 id 없음”을 구분하기가 애매할 수 있습니다.

도메인에서 이 함수를 호출하는 쪽과 논의해,

  • “id가 반드시 내려와야 한다”는 계약이라면 nil 대신 명시적인 도메인 에러를 throw 하거나,
  • “id가 없을 수도 있다”는 스펙이라면 그 의미를 주석/문서에 남겨 두는 것
    을 고려해 보시면 좋겠습니다.
Projects/Domain/Sources/Entity/Enum/ReportType.swift (1)

8-18: rawValue가 이미 대문자라면 uppercased()는 불필요합니다

지금 케이스들의 rawValue"TRANSPORTATION", "LIGHTING" 등 이미 모두 대문자라서, description에서 한 번 더 uppercased()를 호출할 필요는 없습니다. 동작에는 문제가 없지만, 간결성을 위해 아래처럼 단순화할 수 있습니다.

-extension ReportType: CustomStringConvertible {
-    public var description: String {
-        return self.rawValue.uppercased()
-    }
-}
+extension ReportType: CustomStringConvertible {
+    public var description: String {
+        rawValue
+    }
+}

추후 소문자 rawValue를 도입해 API용 문자열을 따로 관리하고 싶다면, description 대신 apiValue 같은 별도 프로퍼티를 두는 것도 고려해 볼 수 있습니다.

Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift (1)

83-95: 제보 완료 화면 네비게이션 플로우 및 TODO 정리 제안

reportId 유무에 따라 완료 화면으로 push / 현재 VC pop 처리하는 delegate 플로우는 자연스럽습니다.
다만 Line 90의 TODO는 이미 reportId를 생성자 인자로 전달하고 있어 다소 중복·모호해 보이니, 최종적으로 id만 넘길지/엔티티 전체를 넘길지 결정된 뒤 TODO를 제거하거나 더 구체적인 FIXME 또는 이슈 링크로 정리해 두면 좋겠습니다.
아울러 ReportDetailViewModel 의존성이 Domain 쪽 DIAssembler에 정상 등록돼 있는지만 한 번만 확인 부탁드립니다.

Projects/Presentation/Sources/Home/View/HomeViewController.swift (1)

667-677: Report 제보 플로우의 ViewModel/문구 불일치

ReportRegistrationViewModel을 resolve 해서 ReportRegistrationViewController로 push 하는 구성은 새 제보 플로우와 일관됩니다.
다만 fatalError 메시지에는 "reportRegistrationViewController 의존성"이라고 되어 있어 실제 resolve하는 타입(ViewModel)과 이름이 맞지 않으니, "reportRegistrationViewModel 의존성이 등록되지 않았습니다."처럼 문구를 맞춰 두면 디버깅 시 더 명확할 것 같습니다.

Projects/Domain/Sources/Protocol/UseCase/ReportUseCaseProtocol.swift (1)

8-38: ReportUseCaseProtocol 메서드 시그니처/주석 정합성 양호

fetchReports / fetchReport / report(title:content:category:location:photos:) 추가가 실제 ReportUseCase 구현과 잘 맞고, 사진 업로드까지 포함한 도메인 요구사항이 명확히 드러납니다.
선택 사항이지만, fetchCurrentLocation·fetchReport처럼 nil을 반환할 수 있는 메서드에 대해 주석에 "조건 불충족 시 nil" 정도만 추가해 두면 호출 측에서 API를 이해하기 조금 더 수월할 것 같습니다.

Projects/DataSource/Sources/Endpoint/S3UploadEndpoint.swift (1)

10-63: S3Endpoint baseURL/path 및 헤더 조건 한 번만 확인 필요

presigned URL을 그대로 path로 넘기고 baseURL을 빈 문자열로 두는 구현은, 네트워크 레이어에서 baseURL·path를 어떻게 조합하는지에 따라 동작이 달라질 수 있습니다. 만약 URL(string: baseURL + path)처럼 단순 문자열 결합이면 문제 없지만, appendingPathComponent를 사용하는 경우 전체 URL이 잘못 조합될 수 있으니 NetworkService 구현과 함께 한 번만 확인해 주세요.

또한 presigned URL을 생성할 때 Content-Type 등 특정 헤더를 서명에 포함시키는지에 따라, 여기서 headers를 빈 딕셔너리로 두면 업로드가 실패할 가능성도 있습니다. 백엔드와 프로토콜을 맞춰 필요하다면 예를 들어 "Content-Type": "image/jpeg" 같은 헤더를 추가해 두는 편이 안전합니다.

Projects/Domain/Sources/Entity/ReportEntity.swift (1)

19-39: thumbnailURL / photoURLs 필드 추가는 적절하나 이름 일관성 제안

도메인 엔티티에 thumbnailURLphotoURLs를 추가하고 id를 Optional로 바꾼 변경은 상위 레이어(예: 히스토리/상세 뷰모델)에서의 사용 방식과 잘 맞습니다.
다만 이니셜라이저의 파라미터 이름이 photoUrls이고 프로퍼티는 photoURLs라서 대소문자 규칙이 달라 약간 혼동될 수 있습니다. 아래처럼 파라미터 이름도 photoURLs로 맞추면 더 일관될 것 같습니다.

-        thumbnailURL: String?,
-        photoUrls: [String]
+        thumbnailURL: String?,
+        photoURLs: [String]
@@
-        self.thumbnailURL = thumbnailURL
-        self.photoURLs = photoUrls
+        self.thumbnailURL = thumbnailURL
+        self.photoURLs = photoURLs
Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift (2)

23-33: 검증/완료 Publisher의 Subject 설계 한 번만 점검해 보면 좋겠습니다

isReportValidPassthroughSubject로 두면, 구독 시점 이후 이벤트만 전달돼서 중간에 새로 구독하는 쪽은 현재 상태를 못 받을 수 있습니다. 뷰모델 라이프사이클 상 항상 뷰와 함께 생성·바인딩된다면 문제는 없지만, 혹시 재바인딩/재구독 가능성이 있다면 CurrentValueSubject<Bool, Never>로 바꿔 두는 편이 더 안전합니다. 반면 등록 완료 이벤트(reportRegistrationCompleteSubject)는 일회성 이벤트라 PassthroughSubject 선택이 적절해 보입니다.

Also applies to: 37-44, 52-61


134-172: 검증 로직과 register()의 guard 조건을 한 곳으로 모아 두면 유지보수가 더 수월할 것 같습니다

verifyIsReportValid()register()guard가 거의 동일한 조건을 중복해서 가지고 있습니다. 현재는 verifyIsReportValid 쪽만 lat/long까지 확인하고 있어, 나중에 요구사항이 바뀌면 두 곳을 동시에 수정해야 합니다.

예를 들어 “필수값이 모두 채워졌는지 판단하는 private computed property”를 두고, 그 값을 isReportValid 발행과 register()의 guard 양쪽에서 참조하도록 통합하면 조건 누락/불일치 위험이 줄어듭니다.

Also applies to: 174-190

Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift (1)

47-49: 내용 글자 수 카운트 레이블의 최대 글자 수(150)를 상수로 분리해 두는 것이 좋아 보입니다

"150"이 레이블 텍스트("\(content.count) / 150"), UI/기획 스펙 양쪽에서 모두 쓰이는 매직 넘버라서, 예를 들어 Layout 내부나 별도 상수(static let maxContentLength = 150)로 정의해 두고 동일 값을 참조하면 나중에 정책이 바뀔 때 한 군데만 수정하면 되어 유지보수성이 좋아질 것 같습니다.

Also applies to: 67-69, 127-129, 284-294, 371-378

Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift (1)

204-235: bind()에서 summaryStackView 구성은 한 번만 만들고, 이후에는 값만 갱신하는 구조가 더 안전합니다

현재는 reportDetailPublisher가 emit될 때마다 ReportCompleteContent.allCases를 순회하며 makeContentView 결과를 summaryStackView에 계속 추가하고 있습니다. photoStackView는 매번 비워 주고 있지만, summaryStackView는 정리 없이 늘어날 수 있어(emit 1회라는 보장이 깨질 경우) 중복 뷰가 쌓일 여지가 있습니다.

  • configureLayout() 혹은 configureAttribute()에서 ReportCompleteContent에 대한 컨텐츠 뷰를 한 번만 만들고 stack에 추가한 뒤,
  • bind()에서는 각각의 레이블/이미지뷰에 값만 채우는 방식

으로 분리하면, emit 횟수와 관계없이 UI 구조가 안정적으로 유지됩니다.

Projects/Presentation/Sources/Report/ViewModel/ReportHistoryViewModel.swift (2)

34-35: 진행 상태(progress) 필터가 이제 항상 하나만 선택되는 구조인데, 토글 해제 UX가 필요하지 않은지 확인이 필요합니다

filterProgress에서 선택된 progress만 isSelected = true로 만들고, selectedProgress를 항상 해당 값으로 갱신한 뒤 filterReports()에서는

if let selectedProgress,
   selectedProgress != .entire {
    filteredReports = filteredReports.filter { $0.progress == selectedProgress }
}

로 동작하게 되어 있어, 한 번 progress를 누르면 다시 눌러도 해제(=전체로 돌아감)는 되지 않고, 대신 “전체” 칩을 눌러야만 전체 보기로 돌아가게 됩니다.

기획/UX가 “항상 하나의 진행 상태가 선택된 상태를 유지한다”는 의도라면 현재 구현이 적절해 보이고,
이전처럼 같은 항목을 다시 탭하면 필터를 해제하는 토글 UX를 원한다면, 동일 progress를 다시 선택했을 때 selectedProgressnil 또는 .entire로 되돌리는 로직이 필요합니다.

Also applies to: 85-100, 102-117


119-163: fetchReports()의 맵핑/카운트 로직은 도메인 변경에 잘 맞게 정리된 것 같습니다

  • reportEntity.idguard let으로 필터링하고,
  • 날짜 문자열을 Date.convertToDateconvertToString으로 변환한 뒤,
  • thumbnailURLprogress를 그대로 반영해 ReportHistoryItem을 만드는 구조가 도메인 계층 변경과 일관되게 맞춰져 있습니다.
    또한, 이후에 다시 ReportProgress.allCases를 순회하면서 전체/각 상태별 count를 재계산해 progressSubject에 흘려주는 부분도 자연스럽습니다.

별다른 버그 포인트는 안 보이고, 필요하다면 나중에 이 맵핑 로직을 별도 mapper로 분리하는 정도만 고려해 보면 될 것 같습니다.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between abe4b88 and a25dd6f.

⛔ Files ignored due to path filters (6)
  • Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/bitnagil_camera_icon.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/[email protected] is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/[email protected] is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_photo_icon.imageset/bitnagil_photo_icon.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_photo_icon.imageset/[email protected] is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_photo_icon.imageset/[email protected] is excluded by !**/*.png
📒 Files selected for processing (37)
  • Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift (1 hunks)
  • Projects/DataSource/Sources/Common/Enum/Endpoint.swift (2 hunks)
  • Projects/DataSource/Sources/Common/Enum/EndpointBodyType.swift (1 hunks)
  • Projects/DataSource/Sources/DTO/FilePresignedConditionDTO.swift (1 hunks)
  • Projects/DataSource/Sources/DTO/FilePresignedDTO.swift (1 hunks)
  • Projects/DataSource/Sources/DTO/ReportDTO.swift (2 hunks)
  • Projects/DataSource/Sources/Endpoint/FilePresignedEndpoint.swift (1 hunks)
  • Projects/DataSource/Sources/Endpoint/ReportEndpoint.swift (3 hunks)
  • Projects/DataSource/Sources/Endpoint/S3UploadEndpoint.swift (1 hunks)
  • Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift (1 hunks)
  • Projects/DataSource/Sources/NetworkService/NetworkService.swift (1 hunks)
  • Projects/DataSource/Sources/Repository/FileRepository.swift (1 hunks)
  • Projects/DataSource/Sources/Repository/ReportRepository.swift (1 hunks)
  • Projects/Domain/Sources/DomainDependencyAssembler.swift (1 hunks)
  • Projects/Domain/Sources/Entity/Enum/ReportType.swift (1 hunks)
  • Projects/Domain/Sources/Entity/ReportEntity.swift (2 hunks)
  • Projects/Domain/Sources/Protocol/Repository/FileRepositoryProtocol.swift (1 hunks)
  • Projects/Domain/Sources/Protocol/Repository/ReportRepositoryProtocol.swift (1 hunks)
  • Projects/Domain/Sources/Protocol/UseCase/ReportUseCaseProtocol.swift (1 hunks)
  • Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (2 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_photo_icon.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Sources/Common/Component/SelectableItemCell.swift (5 hunks)
  • Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1 hunks)
  • Projects/Presentation/Sources/Common/Protocol/SelectableItem.swift (1 hunks)
  • Projects/Presentation/Sources/Home/View/HomeViewController.swift (1 hunks)
  • Projects/Presentation/Sources/Report/Model/SelectPhotoType.swift (2 hunks)
  • Projects/Presentation/Sources/Report/View/Component/ReportHistory/ReportHistoryTableViewCell.swift (1 hunks)
  • Projects/Presentation/Sources/Report/View/Component/ReportRegistration/ReportTextView.swift (3 hunks)
  • Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift (5 hunks)
  • Projects/Presentation/Sources/Report/View/ReportHistoryViewController.swift (2 hunks)
  • Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift (2 hunks)
  • Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift (13 hunks)
  • Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift (2 hunks)
  • Projects/Presentation/Sources/Report/ViewModel/ReportHistoryViewModel.swift (4 hunks)
  • Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift (5 hunks)
  • Projects/Shared/Sources/Extension/Date+.swift (2 hunks)
✅ Files skipped from review due to trivial changes (4)
  • Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/Contents.json
  • Projects/DataSource/Sources/Common/Enum/EndpointBodyType.swift
  • Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_photo_icon.imageset/Contents.json
  • Projects/DataSource/Sources/Endpoint/ReportEndpoint.swift
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-11-22T11:34:16.094Z
Learnt from: taipaise
Repo: YAPP-Github/Bitnagil-iOS PR: 73
File: Projects/DataSource/Sources/DTO/FilePresignedDTO.swift:8-8
Timestamp: 2025-11-22T11:34:16.094Z
Learning: In the Bitnagil-iOS project, DTOs in the DataSource module like FilePresignedDTO are intentionally kept internal (without public access modifier) when they are only used within the DataSource module and not exposed to other modules.

Applied to files:

  • Projects/DataSource/Sources/DTO/FilePresignedDTO.swift
  • Projects/DataSource/Sources/DTO/FilePresignedConditionDTO.swift
📚 Learning: 2025-07-27T10:05:46.043Z
Learnt from: taipaise
Repo: YAPP-Github/Bitnagil-iOS PR: 25
File: Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationInputView.swift:104-111
Timestamp: 2025-07-27T10:05:46.043Z
Learning: RoutineCreationInputView에서 textField(_:shouldChangeCharactersIn:replacementString:) 메서드가 변경 전 텍스트를 delegate에 전달하는 이유: 변경된 텍스트를 전달하면 텍스트 입력 중에 다른 UI 액션(예: 서브루틴 추가)이 발생할 때 입력 중이던 텍스트가 손실되는 문제가 발생하기 때문. 이는 의도적인 구현으로 동시성 문제를 방지함.

Applied to files:

  • Projects/Presentation/Sources/Report/View/Component/ReportRegistration/ReportTextView.swift
📚 Learning: 2025-11-19T13:49:08.326Z
Learnt from: choijungp
Repo: YAPP-Github/Bitnagil-iOS PR: 68
File: Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift:108-172
Timestamp: 2025-11-19T13:49:08.326Z
Learning: ReportCompleteViewController에서 backgroudView가 fomoImageView와 의도적으로 겹치도록 설계됨. 이를 위해 backgroudView의 top 제약을 fomoImageView.snp.top (또는 fomoImageView)을 기준으로 설정하여 오프셋을 적용함.

Applied to files:

  • Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift
  • Projects/Presentation/Sources/Home/View/HomeViewController.swift
  • Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift
  • Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift
📚 Learning: 2025-07-16T09:09:13.869Z
Learnt from: choijungp
Repo: YAPP-Github/Bitnagil-iOS PR: 19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.

Applied to files:

  • Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift
  • Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift
📚 Learning: 2025-07-16T09:21:15.038Z
Learnt from: choijungp
Repo: YAPP-Github/Bitnagil-iOS PR: 19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.

Applied to files:

  • Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift
  • Projects/Presentation/Sources/Home/View/HomeViewController.swift
🧬 Code graph analysis (17)
Projects/Domain/Sources/Protocol/Repository/FileRepositoryProtocol.swift (1)
Projects/DataSource/Sources/Repository/FileRepository.swift (2)
  • fetchPresignedURL (14-19)
  • uploadFile (21-24)
Projects/Domain/Sources/DomainDependencyAssembler.swift (1)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
  • resolve (18-25)
Projects/Domain/Sources/Protocol/Repository/ReportRepositoryProtocol.swift (2)
Projects/DataSource/Sources/Repository/ReportRepository.swift (1)
  • report (14-39)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (1)
  • report (39-80)
Projects/Presentation/Sources/Report/View/Component/ReportRegistration/ReportTextView.swift (2)
Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)
  • bitnagilChevronIcon (20-22)
Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift (1)
  • reportTextViewDidChanged (520-528)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (2)
Projects/DataSource/Sources/Repository/ReportRepository.swift (3)
  • fetchReports (41-53)
  • fetchReportDetail (55-61)
  • report (14-39)
Projects/DataSource/Sources/Repository/FileRepository.swift (2)
  • fetchPresignedURL (14-19)
  • uploadFile (21-24)
Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift (1)
Projects/DataSource/Sources/NetworkService/Extension/URLRequest+.swift (1)
  • makeBodyParameter (33-36)
Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift (1)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
  • resolve (18-25)
Projects/Presentation/Sources/Home/View/HomeViewController.swift (2)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
  • resolve (18-25)
Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift (1)
  • reportRegistrationViewController (84-95)
Projects/DataSource/Sources/Repository/FileRepository.swift (1)
Projects/DataSource/Sources/NetworkService/NetworkService.swift (1)
  • request (25-48)
Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift (2)
Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift (1)
  • reportRegistrationViewController (84-95)
Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift (2)
  • action (64-81)
  • register (134-172)
Projects/DataSource/Sources/Endpoint/FilePresignedEndpoint.swift (1)
Projects/DataSource/Sources/Repository/FileRepository.swift (1)
  • fetchPresignedURL (14-19)
Projects/DataSource/Sources/Repository/ReportRepository.swift (2)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (1)
  • report (39-80)
Projects/DataSource/Sources/NetworkService/NetworkService.swift (1)
  • request (25-48)
Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift (4)
Projects/Presentation/Sources/Report/View/ReportDetailViewController.swift (3)
  • viewDidLoad (71-74)
  • configureAttribute (76-95)
  • bind (138-145)
Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift (2)
  • action (32-37)
  • fetchReportDetail (39-61)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
  • resolve (18-25)
Projects/Presentation/Sources/Common/DesignSystem/Font/BitnagilFont.swift (1)
  • attributedString (50-67)
Projects/Domain/Sources/Protocol/UseCase/ReportUseCaseProtocol.swift (3)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (4)
  • fetchCurrentLocation (25-29)
  • fetchReports (31-33)
  • fetchReport (35-37)
  • report (39-80)
Projects/DataSource/Sources/Repository/ReportRepository.swift (2)
  • fetchReports (41-53)
  • report (14-39)
Projects/Presentation/Sources/Report/ViewModel/ReportHistoryViewModel.swift (1)
  • fetchReports (119-167)
Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift (2)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (2)
  • fetchCurrentLocation (25-29)
  • report (39-80)
Projects/DataSource/Sources/Repository/ReportRepository.swift (1)
  • report (14-39)
Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift (1)
Projects/Shared/Sources/Extension/Date+.swift (2)
  • convertToDate (24-30)
  • convertToString (16-22)
Projects/Presentation/Sources/Report/ViewModel/ReportHistoryViewModel.swift (1)
Projects/Shared/Sources/Extension/Date+.swift (2)
  • convertToDate (24-30)
  • convertToString (16-22)
🪛 SwiftLint (0.57.0)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift

[Warning] 50-50: TODOs should be resolved (- 사진 업로드 실패 시 에러 처리 필요)

(todo)

Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift

[Warning] 90-90: TODOs should be resolved (- reportCompleteViewController...)

(todo)

🔇 Additional comments (25)
Projects/Presentation/Sources/Report/View/Component/ReportRegistration/ReportTextView.swift (2)

29-30: 쉐브론 아이콘 업데이트가 적절합니다.

디자인 시스템 개선으로 아이콘 크기와 에셋 참조가 업데이트되었습니다. 변경사항이 명확하고 구현이 정확합니다.

Also applies to: 59-59


161-161: 실시간 텍스트 업데이트로 인한 중복 호출 및 성능 검증이 필요합니다.

검증 결과, textViewDidChange(Line 161)와 textViewDidEndEditing(Line 166)에서 동일한 delegate 메서드가 호출됩니다. 사용자가 편집을 종료할 때 마지막 입력으로 인한 호출(Line 161) + 편집 종료 호출(Line 166)이 중복 발생합니다.

또한 ReportRegistrationViewModelconfigureTitle/configureContent 메서드에서 매 입력마다 verifyIsReportValid()를 호출하고 있습니다. 다음을 확인해주세요:

  1. 중복 호출 의도 확인: 두 시점의 호출이 모두 필요한지, 아니면 하나로 통합해야 하는지 검토
  2. 성능 영향 평가: verifyIsReportValid() 구현에서 무거운 작업(유효성 검사, 네트워크 호출 등)이 있는 경우 디바운싱 적용 고려
  3. 팀 정책 확인: 다른 입력 뷰와의 일관성 검토 (예: RoutineCreationInputView의 패턴과 비교)
Projects/Shared/Sources/Extension/Date+.swift (1)

35-36: 새 DateType 포맷 추가 적절합니다

yearMonthDateWeek, yearMonthDateWeek2 포맷이 기존 패턴(yyyy-MM-dd, yy.MM.dd)과 잘 맞고, ko_KR 로케일에서 요일을 짧게 보여주기에도 무난해 보입니다.
해당 케이스를 사용하는 ViewModel 쪽에서 문자열 파싱(convertToDate)까지 필요하다면, 역변환에 사용되는 포맷도 동일한지 정도만 한 번 더 점검해 주세요.

Also applies to: 49-50

Projects/Presentation/Sources/Common/Protocol/SelectableItem.swift (1)

8-9: SelectableItemicon 추가 설계 괜찮습니다

프로토콜에 icon: UIImage? 를 추가하면서, 기본 구현에서 nil 을 반환하도록 해서 기존 conformer 들이 깨지지 않는 점이 좋습니다.
이 프로토콜이 Presentation 모듈에만 쓰인다면 UIKit 의존성도 수용 가능한 수준이라 보입니다. 다른 모듈(예: Domain)로 확장할 계획이 있다면, 그때는 아이콘 정보를 별도의 value type 으로 추상화하는 정도만 염두에 두면 될 것 같습니다.

Also applies to: 14-19

Projects/DataSource/Sources/Common/Enum/Endpoint.swift (1)

8-9: Endpoint의 bodyType/bodyData 확장은 방향성 좋습니다

JSON 기반 기존 엔드포인트는 bodyType = .json, bodyParameters 만 사용하고, 파일 업로드 등은 bodyType = .rawData, bodyData 를 사용하는 구조로 잘 분리된 것 같습니다.
다만 Endpoint+.swift 에서 bodyType 에 따라 httpBody 생성 시 bodyParametersbodyData 가 동시에 사용되지 않도록(하나는 반드시 무시되도록) 분기 처리가 되어 있는지만 한 번만 확인해 주세요.

Also applies to: 18-25

Projects/DataSource/Sources/DTO/ReportDTO.swift (4)

10-10: LGTM!

Codable 채택이 적절합니다. ReportDTO가 이제 네트워크 요청 본문(인코딩)과 응답(디코딩) 모두에 사용되므로, Decodable에서 Codable로의 변경이 타당합니다.


16-16: LGTM!

reportStatus를 옵셔널로 변경하고 안전한 기본값 매핑을 사용하는 것이 적절합니다. 이중 폴백 패턴(?? "" ?? .received)이 nil 또는 유효하지 않은 값을 안전하게 처리합니다.

Also applies to: 29-29, 46-46


35-35: LGTM!

reportImageUrlthumbnailURL로 매핑하는 것이 적절하며, 도메인 엔티티의 업데이트와 일관성을 유지합니다.

Also applies to: 52-52


23-37: reportId guard 불일치는 의도된 설계 패턴입니다.

ReportEntity의 id 필드가 Int?(선택형)로 정의되어 있으므로, 두 메서드의 서로 다른 guard 처리는 의도적입니다:

  • toReportEntity(): reportId를 guard하지 않음 → 새 리포트(서버가 아직 ID를 할당하지 않은 경우)용
  • toReportEntity(date:): guard let reportId 유지 → 기존 리포트(이미 서버에서 ID를 할당받은 경우)용

실제 사용 패턴도 이를 확인해줍니다:

  • 단일 리포트 fetch (ReportRepository:60): 새로운 리포트 처리
  • 그룹화된 과거 리포트 (ReportRepository:48): 기존 리포트 처리

일관성 있는 설계이므로 추가 조치가 필요하지 않습니다.

Projects/Presentation/Sources/Report/View/Component/ReportHistory/ReportHistoryTableViewCell.swift (1)

49-50: LGTM!

셀 선택 스타일을 비활성화하여 탭 시 하이라이트가 표시되지 않도록 합니다. 이는 적절한 UI 개선입니다.

Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift (1)

52-54: LGTM!

FileRepositoryProtocol에 대한 DI 등록이 적절합니다. 기존 등록 패턴과 일관성을 유지하며, 새로운 파일 업로드 기능을 지원합니다.

Projects/DataSource/Sources/DTO/FilePresignedConditionDTO.swift (1)

8-11: LGTM!

FilePresignedConditionDTO 구조가 적절합니다. 프리사인드 URL 요청 조건을 위한 간단하고 명확한 DTO이며, Codable 채택이 올바릅니다. Based on learnings, DataSource 모듈 내에서만 사용되는 DTO는 의도적으로 internal로 유지됩니다.

Projects/DataSource/Sources/NetworkService/NetworkService.swift (1)

81-83: LGTM!

EmptyResponseDTO에 대한 특수 케이스 처리가 적절합니다. 빈 응답(예: 204 No Content)을 반환하는 엔드포인트를 올바르게 처리하며, 기존 디코딩 로직에 영향을 주지 않습니다.

Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift (3)

10-10: LGTM!

Foundation import 추가가 적절합니다. Date 타입 사용을 위해 필요합니다.


53-53: LGTM!

photoUrls에서 photoURLs로 변경이 도메인 엔티티 업데이트와 일치합니다.


43-47: 날짜 포맷 변환 로직 검증 완료

검증 결과:

  • yearMonthDateyearMonthDateWeek2 모두 Date+.swift에 정의됨 (33번, 36번 줄)
  • ✅ 형식 정의가 일치: yearMonthDate = "yyyy-MM-dd"yearMonthDateWeek2 = "yyyy-MM-dd (E)"
  • reportEntity.date가 nil이거나 잘못된 형식일 경우 빈 문자열로 폴백되는 것이 의도된 동작이며, 옵셔널 체이닝과 nil 병합 연산자로 안전하게 처리됨

로직이 정확하고 모든 요구사항이 충족되었습니다.

Projects/Presentation/Sources/Report/View/ReportHistoryViewController.swift (2)

270-270: LGTM!

$0.date 직접 사용이 적절합니다. ReportHistoryItem.date가 이제 non-optional이 되어 ?? "" 폴백이 더 이상 필요하지 않습니다. 이는 날짜 처리 개선과 일관성을 유지합니다.


282-282: 스냅샷 애니메이션 비활성화를 확인해주세요.

animatingDifferencestrue에서 false로 변경되었습니다. 이것이 의도된 것인지 확인해주세요:

  • 성능 최적화를 위한 것인가요?
  • 애니메이션 중 시각적 버그를 방지하기 위한 것인가요?

변경 이유가 명확하지 않다면, 사용자 경험 관점에서 애니메이션을 유지하는 것을 고려해보세요.

Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift (1)

15-22: 검증 완료 - 모든 엔드포인트가 정상적으로 작동합니다.

검증 결과, bodyType 기반 요청 본문 처리 로직은 완벽하게 구현되었습니다:

  • 기본 구현: Endpoint 프로토콜의 확장에서 bodyType: .json, bodyData: nil 기본값 제공으로 8개 엔드포인트가 안전하게 작동
  • rawData 엔드포인트: FilePresignedEndpoint, S3Endpoint 모두 bodyData를 명시적으로 구현하여 올바르게 동작
  • 안전한 처리: switch 문에서 if let data = bodyData 옵셀 체크로 rawData 경우를 안전하게 처리

기존 엔드포인트 모두 호환성을 유지합니다.

Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (1)

31-37: 리포트 조회 관련 메서드는 단순 위임으로 무난합니다

fetchReportsfetchReport(reportId:)가 각각 ReportRepositoryProtocol에 그대로 위임하는 구조는 계층 역할 분리에 잘 맞고, 예외 처리도 상위로 투명하게 전파돼 자연스럽습니다. 별도 수정 필요해 보이지 않습니다.

Projects/DataSource/Sources/DTO/FilePresignedDTO.swift (1)

8-18: FilePresignedDTO는 사용되지 않는 미사용 코드이므로 제거해야 합니다

검색 결과 FilePresignedDTO는 정의되었을 뿐 코드베이스의 어떤 곳에서도 실제로 사용되지 않고 있습니다. 헤더 주석 외에는 참조가 전혀 없으므로 이 구조체는 제거하는 것이 좋겠습니다.

Projects/DataSource/Sources/DTO/FilePresignedDTO.swift 파일 자체를 삭제하거나, 추후 실제 사용할 계획이 있다면 Repository나 Endpoint 계층에서 이 DTO를 활용하도록 연결해 주시기 바랍니다.

⛔ Skipped due to learnings
Learnt from: taipaise
Repo: YAPP-Github/Bitnagil-iOS PR: 73
File: Projects/DataSource/Sources/DTO/FilePresignedDTO.swift:8-8
Timestamp: 2025-11-22T11:34:16.094Z
Learning: In the Bitnagil-iOS project, DTOs in the DataSource module like FilePresignedDTO are intentionally kept internal (without public access modifier) when they are only used within the DataSource module and not exposed to other modules.
Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift (1)

8-8: Shared 모듈 import 추가 적절

Bitnagil* 관련 심볼과 DIContainer를 사용하는 파일이라 Shared import 추가가 필요해 보입니다. 별도 이슈 없습니다.

Projects/Domain/Sources/DomainDependencyAssembler.swift (1)

66-77: ReportUseCase DI에 FileRepository 의존성 추가 OK

locationRepository / reportRepository / fileRepository를 한 번에 resolve 해서 ReportUseCase를 주입하는 구성이 현재 UseCase 초기화 시그니처와 잘 맞습니다. 다만 상위 assembler에서 FileRepositoryProtocol 등록이 누락돼 있으면 여기서 fatalError로 앱이 종료되므로, 해당 등록이 포함돼 있는지만 한 번 확인해 주세요.

Projects/DataSource/Sources/Repository/FileRepository.swift (1)

11-24: FileRepository 구현이 presigned URL + 업로드 플로우와 잘 맞습니다

fetchPresignedURL에서 조건 DTO 배열을 만들어 한 번에 presigned URL을 받아오고, uploadFile에서 S3Endpoint.uploadImage를 통해 rawData PUT 요청을 보내는 구조가 간결하고, 도메인 계층에서 기대하는 FileRepositoryProtocol 인터페이스와도 잘 맞습니다. 현재 수준에서는 별도 수정 없이도 충분해 보입니다.

Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift (1)

284-293: makePhotoView() 구현은 역할 분리가 잘 되어 있습니다

사진 썸네일용 뷰를 별도 팩토리 메서드로 빼고, 크기/코너 라운드/배경색을 한 곳에서 관리하는 구조가 깔끔합니다. 이후 다른 화면에서 동일 스타일 썸네일이 필요해지면 이 메서드를 재사용할 수 있을 것 같습니다.

Comment on lines +29 to +33
var method: HTTPMethod {
switch self {
case .fetchPresignedURL: .post
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

method 프로퍼티에서 return 누락으로 컴파일 에러가 발생합니다

switch 내부에서 .post를 리턴하지 않아 현재 코드는 빌드되지 않습니다. 아래처럼 return을 명시해야 합니다.

    var method: HTTPMethod {
        switch self {
-        case .fetchPresignedURL: .post
+        case .fetchPresignedURL:
+            return .post
        }
    }

추가로, 이 케이스가 하나뿐이라면 switch 대신 단순히 return .post로 구현해도 충분합니다.

🤖 Prompt for AI Agents
In Projects/DataSource/Sources/Endpoint/FilePresignedEndpoint.swift around lines
29 to 33, the computed property `method` currently uses a `switch` but doesn't
return the `.post` value, causing a compile error; fix it by either adding an
explicit `return .post` inside the `case .fetchPresignedURL:` branch or simplify
the implementation to directly `return .post` (preferred if this enum has only
that single case), ensuring the property always returns an HTTPMethod.

Comment on lines +12 to +26
/// 제보를 등록합니다.
/// - Parameters:
/// - title: 제보 제목
/// - content: 제보 내용
/// - category: 제보 카테고리
/// - location: 제보 위치
/// - photos: 업로드한 사진의 presigned urls
/// - Returns: 제보 id
func report(
title: String,
content: String?,
category: ReportType,
location: LocationEntity?,
photoURLs: [String]
) async throws -> Int?
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

report 파라미터 주석이 시그니처 및 실제 값 의미와 맞지 않습니다

현재 주석과 구현 사이에 두 가지 작은 불일치가 있습니다.

  • 파라미터 이름은 photoURLs인데, 주석에는 photos라고 되어 있음
  • 주석에는 “presigned urls”라고 되어 있지만, 실제로 ReportUseCase에서는 publicImageURLs(query 스트립된 공개 URL)을 넘기고 있어, presigned URL이라 보기 어렵습니다

혼동을 줄이기 위해 주석을 아래처럼 맞춰 주는 것을 제안드립니다.

-    ///   - photos: 업로드한 사진의 presigned urls
+    ///   - photoURLs: 업로드가 완료된 사진의 공개 URL 목록
     /// - Returns: 제보 id
     func report(
         title: String,
         content: String?,
         category: ReportType,
         location: LocationEntity?,
-        photoURLs: [String]
+        photoURLs: [String]
     ) async throws -> Int?

이렇게 해 두면, 호출 측에서도 “파일 업로드까지 끝난 뒤 최종적으로 저장되는 URL을 넘긴다”는 의도가 더 명확해질 것 같습니다.

🤖 Prompt for AI Agents
In Projects/Domain/Sources/Protocol/Repository/ReportRepositoryProtocol.swift
around lines 12 to 26, the parameter doc is inconsistent with the signature and
usage: update the @param for photoURLs (replace "photos") and remove "presigned
urls", describing that the method expects final public image URLs
(query-stripped) as used by ReportUseCase; adjust the brief so callers know
these are the final stored/public URLs passed after upload.

Comment on lines 39 to 79
public func report(
title: String,
content: String?,
category: ReportType,
location: LocationEntity?,
photos: [Data]
) async throws -> Int? {
if photos.isEmpty { return nil }

let fileNames = (1...photos.count).map { "\($0).jpg" }

// TODO: - 사진 업로드 실패 시 에러 처리 필요
guard
let presignedDict = try await fileRepository.fetchPresignedURL(prefix: "report", fileNames: fileNames),
presignedDict.count == photos.count
else { return nil }

let presignedURLs = fileNames.compactMap { fileName in
presignedDict[fileName]
}

for (url, photo) in zip(presignedURLs, photos) {
do {
try await fileRepository.uploadFile(url: url, data: photo)
} catch {
print(error.localizedDescription)
}
}

let publicImageURLs = presignedURLs.map { url in
url.split(separator: "?", maxSplits: 1)
.map(String.init)
.first ?? url
}

return try await reportRepository.report(
title: title,
content: content,
category: category,
location: location,
photoURLs: publicImageURLs)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

사진 업로드 실패 시 조용히 진행되는 현재 구현은 정합성 문제가 될 수 있습니다

report(...) 내부에서 사진 업로드 플로우가 다음과 같이 동작하는 점이 조금 위험해 보입니다.

  • fetchPresignedURL 실패/개수 불일치 시에는 nil 반환으로 조용히 종료
  • 업로드 루프에서 각 uploadFile 에러를 print만 하고 무시한 뒤,
    실패 여부와 상관없이 reportRepository.report(...)를 호출
  • 결과적으로 서버에는 실제로 업로드되지 않은 이미지 URL이 포함된 제보가 등록될 수 있음

실제 서비스 관점에서는 “사진이 일부/전부 업로드되지 않았는데도 성공처럼 보이는 제보”가 생길 수 있어, 최소한 전체 업로드 실패 시에는 에러를 throw 하거나, 업로드된 것만 필터링해서 전달하는 등의 처리가 필요해 보입니다. SwiftLint도 TODO를 경고하고 있으니 이 부분을 이번 PR에서 함께 정리하는 것이 좋겠습니다.

한 가지 단순한 개선안은 “업로드 실패 시 전체 제보 등록을 중단하고 에러를 전파”하는 것입니다.

-        // TODO: - 사진 업로드 실패 시 에러 처리 필요
-        guard
-            let presignedDict = try await fileRepository.fetchPresignedURL(prefix: "report", fileNames: fileNames),
-            presignedDict.count == photos.count
-        else { return nil }
+        guard
+            let presignedDict = try await fileRepository.fetchPresignedURL(prefix: "report", fileNames: fileNames),
+            presignedDict.count == photos.count
+        else {
+            // presigned URL 개수 불일치는 로직/서버 문제에 가까우므로 에러로 보고합니다.
+            throw ReportError.invalidPresignedURLCount
+        }
@@
-        for (url, photo) in zip(presignedURLs, photos) {
-            do {
-                try await fileRepository.uploadFile(url: url, data: photo)
-            } catch {
-                print(error.localizedDescription)
-            }
-        }
+        for (url, photo) in zip(presignedURLs, photos) {
+            // 업로드 실패 시 바로 throw 되어 전체 제보 등록을 중단합니다.
+            try await fileRepository.uploadFile(url: url, data: photo)
+        }

위 예시의 ReportError.invalidPresignedURLCount는 도메인 에러 enum 등을 새로 정의해야 하므로, 실제 구현에서는 프로젝트에서 이미 사용 중인 에러 타입에 맞춰 조정해 주시면 됩니다.

추가로:

  • photos.isEmpty { return nil } 도 “사진이 필수인지 여부”에 따라 throw로 validation 실패를 알려주는 쪽이 더 디버깅에 유리할 수 있습니다.
  • presignedURLs.count == photos.count 여부를 한 번 더 guard로 확인해 두면, key mismatch 시 일부 사진만 업로드되는 상황도 방지할 수 있습니다.

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 50-50: TODOs should be resolved (- 사진 업로드 실패 시 에러 처리 필요)

(todo)

Comment on lines 99 to 110
private func configureLocation() {
Task {
do {
self.location = try await reportUseCase.fetchCurrentLocation()
locationSubject.send(location?.address)
verifyIsReportValid()
} catch {

locationSubject.send(nil)
verifyIsReportValid()
}

locationSubject.send(location?.address)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

위치 조회 실패 시 location 프로퍼티가 초기화되지 않아 검증/실제 등록 상태가 어긋날 수 있습니다

configureLocation()catch 블록에서 locationSubjectnil을 보내지만, self.location은 그대로 유지됩니다. 이 상태에서 verifyIsReportValid()는 이전에 성공했던 위치값을 계속 보고 true를 줄 수 있고, register()도 이전 위치로 신고를 보냅니다. UI에는 위치가 비어 보이는데 실제로는 이전 좌표로 신고되는 상황이 생길 수 있습니다.

에러 시 self.location도 함께 nil로 초기화하는 쪽이 더 일관될 것 같습니다.

    private func configureLocation() {
        Task {
            do {
                self.location = try await reportUseCase.fetchCurrentLocation()
                locationSubject.send(location?.address)
                verifyIsReportValid()
            } catch {
-               locationSubject.send(nil)
-               verifyIsReportValid()
+               self.location = nil
+               locationSubject.send(nil)
+               verifyIsReportValid()
            }
        }
    }

또한 검증 로직(verifyIsReportValid)에서 lat/long까지 체크해 주고 있으니, 이 상태라면 등록/버튼 활성 조건 모두 동일하게 맞춰집니다.

Also applies to: 174-190

🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift
around lines 99-110 (and similarly 174-190), the catch path for
configureLocation() only sends nil to locationSubject but leaves self.location
unchanged, causing validation and registration to use stale coordinates; update
the catch block(s) to also set self.location = nil before sending
locationSubject.send(nil) and calling verifyIsReportValid(), and ensure
verifyIsReportValid() validates presence of latitude/longitude (not just non-nil
location) so button enablement and register() use the same lat/long checks.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift (1)

300-311: 레이아웃 제약 조건을 수정해야 합니다.

locationTitleLabel의 top 제약이 reportContentTextView.snp.bottom을 참조하고 있지만, 이제 contentTextCountLabel이 그 사이에 추가되었습니다. Lines 288-298에서 contentTextCountLabelreportContentTextView 아래에 배치했으므로, locationTitleLabelcontentTextCountLabel.snp.bottom을 기준으로 배치되어야 합니다. 현재 상태로는 두 요소가 겹치거나 contentTextCountLabel이 올바른 위치에 표시되지 않습니다.

다음과 같이 수정하세요:

        locationTitleLabel.snp.makeConstraints { make in
            make.top
-                .equalTo(reportContentTextView.snp.bottom)
+                .equalTo(contentTextCountLabel.snp.bottom)
                .offset(Layout.titleLabelTopSpacing)

            make.leading
                .equalToSuperview()
                .offset(Layout.horizontalInset)

            make.height
                .equalTo(Layout.titleLabelHeight)
        }
♻️ Duplicate comments (4)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (2)

50-62: 이전 리뷰에서 지적된 에러 처리 문제가 여전히 남아있습니다

사진 업로드 실패 시 조용히 진행되는 현재 구현(TODO 주석, silent nil 반환, print만 하는 에러 처리)은 이전 리뷰 코멘트에서 이미 상세히 지적되었습니다. 업로드 실패 시 전체 제보 등록을 중단하고 에러를 전파하는 방식으로 개선이 필요합니다.

이전 리뷰 코멘트를 참고하여 에러 처리를 개선해 주세요.


46-46: 사진 필수 여부 확인 필요

사진 배열이 비어있을 때 nil을 반환하는데, 이것이 의도된 동작인지 확인이 필요합니다. 만약 제보에 사진이 필수라면 에러를 throw하여 validation 실패를 명확히 알리는 것이 디버깅에 유리합니다.

사진이 필수인 경우 다음과 같이 수정할 수 있습니다:

-        if photos.isEmpty { return nil }
+        guard !photos.isEmpty else {
+            throw ReportError.photosRequired
+        }
Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift (1)

111-114: 이전 리뷰 지적사항이 여전히 해결되지 않았습니다

이전 리뷰에서 지적된 문제가 그대로 남아있습니다. catch 블록에서 locationSubject에만 nil을 전송하고 self.location은 초기화하지 않아, 이전에 성공했던 위치 값이 유지될 수 있습니다. 현재 verifyIsReportValid()에서 위도/경도를 검증하고 있지만, 명시적으로 self.location = nil로 초기화하는 것이 더 안전합니다.

 private func configureLocation() {
     Task {
         do {
             self.location = try await reportUseCase.fetchCurrentLocation()
             locationSubject.send(location?.address)
             verifyIsReportValid()
         } catch {
+            self.location = nil
             locationSubject.send(nil)
             verifyIsReportValid()
         }
     }
 }
Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift (1)

83-87: configureAttribute/configureLayout 중복 호출 이슈가 여전히 남아 있습니다

BaseViewControllerviewDidLoad()에서 이미 configureAttribute(), configureLayout(), bind()를 호출하는 패턴이라면, 여기서 다시 호출하면 서브뷰/제약이 중복으로 잡힐 수 있습니다. Auto Layout 경고나 예상치 못한 UI 깨짐이 나올 수 있어, 이 VC에서도 한 번만 호출되도록 정리하는 편이 안전해 보입니다.

아래처럼 viewDidLoad에서는 액션만 트리거하도록 바꾸는 걸 제안드립니다.

override func viewDidLoad() {
    super.viewDidLoad()
-    configureAttribute()
-    configureLayout()
    viewModel.action(input: .fetchReportDetail(reportId: reportId))
}

Based on learnings, BaseViewController.viewDidLoad()에서 이미 configureAttribute/configureLayout/bind()를 호출하는 것으로 알고 있습니다.

🧹 Nitpick comments (3)
Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift (3)

121-141: 탭바 VC 인덱스/캐스팅에 대한 방어 로직을 두는 편이 안전합니다

tabBarController.viewControllers?[0/1/2]에 바로 인덱스로 접근하고 UINavigationController로 캐스팅하는 구현은, 탭 구성이 바뀌거나 VC 수가 줄어들면 인덱스 범위 초과로 크래시가 날 수 있습니다.

예를 들어 아래처럼 viewControllers 개수와 타입을 한 번에 검증하는 쪽을 고려해 볼 수 있습니다.

guard
    let tabBarController = self.tabBarController,
    let viewControllers = tabBarController.viewControllers,
    viewControllers.count >= 3,
    let homeNav = viewControllers[0] as? UINavigationController,
    let routineNav = viewControllers[1] as? UINavigationController,
    let myPageNav = viewControllers[2] as? UINavigationController,
    let reportHistoryViewModel = DIContainer.shared.resolve(type: ReportHistoryViewModel.self)
else {
    self?.navigationController?.popToRootViewController(animated: true)
    return
}

homeNav.popToRootViewController(animated: false)
routineNav.popToRootViewController(animated: false)
tabBarController.selectedIndex = 2
myPageNav.pushViewController(ReportHistoryViewController(viewModel: reportHistoryViewModel), animated: true)

현재 구조가 고정이라면 동작에는 문제가 없겠지만, 탭 구성이 바뀔 수 있는 앱이라면 위와 같이 방어 로직을 추가하는 것을 추천드립니다.


206-237: summaryStackView에 컨텐츠가 재바인딩 시 중복 추가될 수 있습니다

bind()에서:

ReportCompleteContent.allCases.forEach { ... }
self?.summaryStackView.addArrangedSubview(contentStackView ?? UIView())

를 매 emit마다 실행하지만, summaryStackView의 기존 컨텐츠(특히 첫 바인딩 때 추가한 행들)를 지우지 않고 있습니다. reportDetailPublisher가 여러 번 emit될 가능성이 있다면, 동일한 행이 반복해서 쌓일 수 있습니다. (photoStackView는 이미 잘 비워주고 있어서 동일 패턴만 summaryStackView에 적용하면 될 것 같습니다.)

한 번만 레이아웃을 구성하고 이후에는 값만 업데이트하는 식으로 바꾸면 더 안전합니다. 예를 들면:

private var didSetupSummaryContent = false

override func bind() {
    viewModel.output.reportDetailPublisher
        .receive(on: DispatchQueue.main)
        .sink { [weak self] reportDetail in
            guard let self, let reportDetail else { return }

            if !self.didSetupSummaryContent {
                ReportCompleteContent.allCases.forEach { type in
                    let contentStackView = self.makeContentView(contentType: type)
                    self.summaryStackView.addArrangedSubview(contentStackView)
                }
                self.didSetupSummaryContent = true
            }

            // 이후 title/category/location/description, photoStackView 업데이트
        }
        .store(in: &cancellables)
}

또한 makeContentView는 non-optional을 반환하므로 contentStackView ?? UIView() 부분은 불필요하고, 위처럼 guard let self 이후에는 self를 강한 참조로 사용하는 편이 코드도 더 단순해집니다.


60-61: backgroudView 프로퍼티 네이밍 오타

backgroudViewbackgroundView로 보이는 오타입니다. 동작에는 문제가 없지만, 추후 검색/리팩토링 시 혼동을 줄이기 위해 한 번에 이름을 정리해 두는 편이 좋겠습니다.

예:

-private let backgroudView = UIView()
+private let backgroundView = UIView()
...
- backgroudView.backgroundColor = BitnagilColor.gray98
- backgroudView.layer.masksToBounds = true
- backgroudView.layer.cornerRadius = 12
+ backgroundView.backgroundColor = BitnagilColor.gray98
+ backgroundView.layer.masksToBounds = true
+ backgroundView.layer.cornerRadius = 12
...
- backgroudView.snp.makeConstraints { make in
+ backgroundView.snp.makeConstraints { make in
    ...
}

규모가 큰 변경은 아니라, 이번 PR이나 후속 UI 리팩토링 때 함께 정리해도 좋을 것 같습니다.

Also applies to: 107-110, 187-191

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a25dd6f and d3f482e.

📒 Files selected for processing (6)
  • Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (2 hunks)
  • Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift (1 hunks)
  • Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift (5 hunks)
  • Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift (16 hunks)
  • Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift (7 hunks)
  • Projects/Shared/Sources/Extension/Date+.swift (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Projects/Shared/Sources/Extension/Date+.swift
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-07-16T09:21:15.038Z
Learnt from: choijungp
Repo: YAPP-Github/Bitnagil-iOS PR: 19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.

Applied to files:

  • Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift
📚 Learning: 2025-11-19T13:49:08.326Z
Learnt from: choijungp
Repo: YAPP-Github/Bitnagil-iOS PR: 68
File: Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift:108-172
Timestamp: 2025-11-19T13:49:08.326Z
Learning: ReportCompleteViewController에서 backgroudView가 fomoImageView와 의도적으로 겹치도록 설계됨. 이를 위해 backgroudView의 top 제약을 fomoImageView.snp.top (또는 fomoImageView)을 기준으로 설정하여 오프셋을 적용함.

Applied to files:

  • Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift
  • Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift
  • Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift
📚 Learning: 2025-07-16T09:09:13.869Z
Learnt from: choijungp
Repo: YAPP-Github/Bitnagil-iOS PR: 19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.

Applied to files:

  • Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift
🧬 Code graph analysis (4)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (2)
Projects/DataSource/Sources/Repository/ReportRepository.swift (3)
  • fetchReports (41-53)
  • fetchReportDetail (55-61)
  • report (14-39)
Projects/DataSource/Sources/Repository/FileRepository.swift (2)
  • fetchPresignedURL (14-19)
  • uploadFile (21-24)
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift (2)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
  • resolve (18-25)
Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift (1)
  • reportRegistrationViewController (84-95)
Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift (2)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (2)
  • fetchCurrentLocation (25-29)
  • report (39-76)
Projects/DataSource/Sources/Repository/ReportRepository.swift (1)
  • report (14-39)
Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift (4)
Projects/Presentation/Sources/Report/View/ReportDetailViewController.swift (1)
  • viewDidLoad (71-74)
Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift (2)
  • action (32-37)
  • fetchReportDetail (39-61)
Projects/DataSource/Sources/Repository/ReportRepository.swift (1)
  • fetchReportDetail (55-61)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
  • resolve (18-25)
🪛 SwiftLint (0.57.0)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift

[Warning] 50-50: TODOs should be resolved (- 사진 업로드 실패 시 에러 처리 필요)

(todo)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (9)
Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift (1)

180-196: 검증 로직이 잘 구현되었습니다

모든 필수 필드(제목, 카테고리, 내용, 위치 및 좌표, 사진)를 포괄적으로 검증하고 있으며, 특히 위도/경도 확인을 추가하여 이전 리뷰의 우려사항을 해결했습니다.

Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift (8)

14-16: LGTM!

델리게이트 프로토콜이 적절하게 정의되었으며, 제보 등록 완료 시 reportId를 전달하는 구조가 명확합니다.


120-125: LGTM!

로딩 화면을 먼저 표시한 후 등록 액션을 트리거하는 플로우가 적절합니다. 델리게이트 패턴을 통해 완료 시점을 비동기로 전달하는 구조가 잘 설계되었습니다.


133-135: LGTM!

키보드 dismiss 제스처 추가로 사용자 경험이 개선되었습니다. cancelsTouchesInView = false 설정으로 다른 터치 이벤트를 방해하지 않는 점도 적절합니다.

Also applies to: 537-539


209-272: LGTM!

폼 필드 순서를 재구성하여 제목 → 카테고리 → 내용 순으로 변경한 것이 사용자 경험 측면에서 자연스러운 흐름입니다. 제약 조건이 새로운 레이아웃에 맞게 올바르게 설정되었습니다.


411-424: LGTM!

사진 최대 개수 검증 로직이 적절하게 구현되었습니다. 사용자가 최대 개수에 도달했을 때 명확한 알림을 제공하고, 그렇지 않은 경우에만 카메라/앨범 선택 화면을 표시하는 플로우가 잘 설계되었습니다.


426-435: LGTM!

폼 유효성 검사를 반응형으로 처리하여 제출 버튼의 상태를 동적으로 업데이트하는 구현이 적절합니다. 사용자가 필수 항목을 입력하기 전까지 제출을 방지하는 좋은 UX 패턴입니다.


437-444: LGTM!

등록 완료 시 델리게이트를 통해 reportId를 전달하는 구조가 명확하고 적절합니다. 비동기 완료를 처리하는 패턴이 잘 구현되었습니다.


621-637: LGTM!

PHPicker에서 여러 사진을 선택할 수 있도록 개선되었습니다. 각 결과를 개별적으로 처리하며, 이미지 로드가 실패한 경우 적절히 스킵하는 로직이 잘 구현되었습니다. 다만, ViewModel에서 총 사진 개수가 최대값을 초과하지 않도록 검증하는 로직이 있는지 확인하세요.

) async throws -> Int? {
if photos.isEmpty { return nil }

let fileNames = (1...photos.count).map { "\($0).jpg" }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

파일명 충돌 위험: 고유 식별자 추가 필요

순차 번호(1.jpg, 2.jpg)만으로 파일명을 생성하면 여러 사용자가 동시에 업로드할 때 파일명이 중복될 수 있습니다. S3 prefix로 report를 사용하더라도 같은 시간대에 업로드되는 파일들 간 충돌이 발생할 수 있습니다.

UUID나 타임스탬프를 포함한 고유 파일명 생성 방식으로 변경하는 것을 권장합니다.

-        let fileNames = (1...photos.count).map { "\($0).jpg" }
+        let timestamp = Int(Date().timeIntervalSince1970 * 1000)
+        let fileNames = (1...photos.count).map { "\(timestamp)_\($0).jpg" }

또는 UUID를 사용하는 방법:

-        let fileNames = (1...photos.count).map { "\($0).jpg" }
+        let fileNames = (1...photos.count).map { "\(UUID().uuidString).jpg" }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let fileNames = (1...photos.count).map { "\($0).jpg" }
let timestamp = Int(Date().timeIntervalSince1970 * 1000)
let fileNames = (1...photos.count).map { "\(timestamp)_\($0).jpg" }
Suggested change
let fileNames = (1...photos.count).map { "\($0).jpg" }
let fileNames = (1...photos.count).map { "\(UUID().uuidString).jpg" }
🤖 Prompt for AI Agents
In Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift around line 48,
the code generates filenames as "1.jpg", "2.jpg" which risks collisions when
multiple users/upload batches occur; change the filename generation to include a
unique identifier (e.g., append/prepend a UUID or timestamp to each name) so
each photo becomes something like "<uuid>_<index>.jpg" or
"<timestamp>_<index>.jpg"; ensure the chosen identifier is generated per file
(not per batch unless acceptable), preserve the .jpg extension, and update any
downstream code that relies on the filename format.

Comment on lines +56 to +62
for (url, photo) in zip(presignedDict.values, photos) {
do {
try await fileRepository.uploadFile(url: url, data: photo)
} catch {
print(error.localizedDescription)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

치명적 버그: Dictionary 순서 불일치로 인한 사진-URL 매핑 오류

Swift의 Dictionary.values는 순서가 보장되지 않습니다. Line 56에서 presignedDict.valuesphotos 배열과 zip으로 매핑하면, 실제 의도한 사진과 다른 presigned URL로 업로드될 수 있습니다. 예를 들어 1.jpg용 URL에 2.jpg 데이터가 업로드되는 등의 데이터 정합성 문제가 발생합니다.

파일명을 키로 사용하여 올바른 URL과 사진을 매핑해야 합니다:

-        for (url, photo) in zip(presignedDict.values, photos) {
+        for (fileName, photo) in zip(fileNames, photos) {
+            guard let url = presignedDict[fileName] else {
+                throw FileUploadError.missingPresignedURL
+            }
             do {
                 try await fileRepository.uploadFile(url: url, data: photo)
             } catch {
                 print(error.localizedDescription)
             }
         }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift around lines
56–62, the code zips presignedDict.values with photos which is unsafe because
Dictionary.values is unordered; instead, map each photo to its presigned URL
using the filename (or the exact key used in presignedDict) and upload by
looking up presignedDict[photoFilename] for each photo, handling the missing-URL
case (log/throw) and keeping the await/try semantics inside that loop so each
photo is uploaded to its correct URL.

Comment on lines +64 to +68
let publicImageURLs = presignedDict.values.map { url in
url.split(separator: "?", maxSplits: 1)
.map(String.init)
.first ?? url
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Dictionary 순서 불일치: 공개 URL 배열의 순서가 보장되지 않음

Line 64에서도 presignedDict.values를 사용하여 공개 URL 배열을 생성하는데, Dictionary의 순서가 보장되지 않아 실제 업로드된 사진과 다른 순서의 URL이 서버에 전달될 수 있습니다.

파일명 순서를 기준으로 URL을 추출해야 합니다:

-        let publicImageURLs = presignedDict.values.map { url in
+        let publicImageURLs = fileNames.compactMap { fileName in
+            guard let url = presignedDict[fileName] else { return nil }
+            return url.split(separator: "?", maxSplits: 1)
-            url.split(separator: "?", maxSplits: 1)
                 .map(String.init)
                 .first ?? url
         }
🤖 Prompt for AI Agents
In Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift around lines 64 to
68, the code uses presignedDict.values which does not guarantee order and can
misalign URLs with uploaded photos; instead build the publicImageURLs array by
iterating over the expected file order (e.g., the original filenames array or
sorted keys) and look up each filename in presignedDict to produce URLs in the
correct sequence, handling missing entries gracefully (skip or provide fallback)
so the resulting array matches the photo upload order.

Comment on lines 338 to 348
func floatingMenuDidTapReportButton(_ sender: FloatingMenuView) {
toggleFloatingButton()
// TODO: 제보하기 뷰로 이동

guard let reportRegistrationViewModel = DIContainer.shared.resolve(type: ReportRegistrationViewModel.self)
else { fatalError("reportRegistrationViewController 의존성이 등록되지 않았습니다.") }

let reportRegistrationViewController = ReportRegistrationViewController(viewModel: reportRegistrationViewModel)
reportRegistrationViewController.hidesBottomBarWhenPushed = true

self.navigationController?.pushViewController(reportRegistrationViewController, animated: true)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

오류 메시지 수정이 필요합니다.

Line 342의 fatalError 메시지가 실제로 resolve하는 타입과 일치하지 않습니다. ReportRegistrationViewModel을 resolve하고 있지만 메시지는 "reportRegistrationViewController"라고 표시되어 있어 디버깅 시 혼란을 줄 수 있습니다.

다음과 같이 수정하세요:

        guard let reportRegistrationViewModel = DIContainer.shared.resolve(type: ReportRegistrationViewModel.self)
-        else { fatalError("reportRegistrationViewController 의존성이 등록되지 않았습니다.") }
+        else { fatalError("reportRegistrationViewModel 의존성이 등록되지 않았습니다.") }
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift
around lines 338–348, the fatalError on line 342 logs the wrong dependency name
("reportRegistrationViewController") while resolving
ReportRegistrationViewModel; update the error message to reference the actual
type being resolved (e.g., "ReportRegistrationViewModel 의존성이 등록되지 않았습니다.") so
the log accurately reflects the missing dependency and aids debugging.

Comment on lines +380 to +381
let content = content ?? ""
self?.contentTextCountLabel.text = "\(content.count) / 150"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for ViewModel file related to Report Registration
find Projects -name "*ViewModel*" -type f | head -20

Repository: YAPP-Github/Bitnagil-iOS

Length of output: 1301


🏁 Script executed:

#!/bin/bash
# Search for content length validation and UITextView handling
rg -n "150|maxLength|max.*length|inputContent|content.*limit" --type=swift -i | head -40

Repository: YAPP-Github/Bitnagil-iOS

Length of output: 2218


🏁 Script executed:

#!/bin/bash
# Look for ReportRegistrationViewController class definition and content handling
rg -n "class ReportRegistrationViewController|contentPublisher|UITextViewDelegate" --type=swift -B2 -A2

Repository: YAPP-Github/Bitnagil-iOS

Length of output: 3960


🏁 Script executed:

#!/bin/bash
# Read ReportRegistrationViewModel to check for content validation logic
cat -n Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift

Repository: YAPP-Github/Bitnagil-iOS

Length of output: 8338


🏁 Script executed:

#!/bin/bash
# Read ReportTextView to check UITextViewDelegate implementation
cat -n Projects/Presentation/Sources/Report/View/Component/ReportRegistration/ReportTextView.swift

Repository: YAPP-Github/Bitnagil-iOS

Length of output: 6471


🏁 Script executed:

#!/bin/bash
# Check ReportRegistrationViewController around lines 375-381 for full context
sed -n '360,400p' Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift | cat -n

Repository: YAPP-Github/Bitnagil-iOS

Length of output: 1966


Add 150-character validation for report content input.

The UI displays character count as "count / 150", but the ViewModel does not enforce this limit. The configureContent() method (line 100-103 in ReportRegistrationViewModel.swift) accepts any length without truncation or validation. Add shouldChangeTextIn delegate method to ReportTextView to prevent input exceeding 150 characters, similar to the pattern used in WithdrawViewController with its 100-character limit.

🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift
around lines 380-381, the UI shows "count / 150" but input is not limited; add a
UITextViewDelegate implementation for ReportTextView
(shouldChangeTextIn:range:replacementText:) that computes the prospective text
length and returns false if it would exceed 150 characters to prevent further
input, mirror the pattern used in WithdrawViewController for the 100-char limit;
also update ReportRegistrationViewModel.configureContent() (lines ~100-103 in
ReportRegistrationViewModel.swift) to enforce/truncate to 150 characters on
programmatic changes (trim incoming content to 150, assign trimmed string, and
update the content count binding) so both user typing and programmatic sets
never exceed 150 and the contentTextCountLabel remains accurate.

Comment on lines +141 to +148
guard
let name = titleSubject.value,
!name.isEmpty,
let category = categorySubject.value,
let content = contentSubject.value,
let location,
selectedPhotoSubject.value.count > 0
else { return }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

검증 로직이 verifyIsReportValid()와 불일치합니다

register()의 가드문은 location의 존재만 확인하고 위도/경도는 검증하지 않습니다. 반면 verifyIsReportValid() (187-188번 줄)는 location.latitudelocation.longitudenil이 아닌지 확인합니다.

이로 인해 버튼은 활성화되었지만, location에 좌표가 없는 경우 등록 시 문제가 발생할 수 있습니다. 일관성을 위해 register()의 가드문에도 동일한 위도/경도 검증을 추가해야 합니다.

 private func register() {
     guard
         let name = titleSubject.value,
         !name.isEmpty,
         let category = categorySubject.value,
         let content = contentSubject.value,
         let location,
+        location.latitude != nil,
+        location.longitude != nil,
         selectedPhotoSubject.value.count > 0
     else { return }
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift
around lines 141-148, the guard in register() only checks that location exists
but does not validate location.latitude and location.longitude like
verifyIsReportValid() does (lines ~187-188), causing inconsistent validation;
update the guard to also unwrap/verify that location.latitude and
location.longitude are non-nil (or use the same validation helper used by
verifyIsReportValid()) before proceeding so register() and verifyIsReportValid()
remain consistent.

Comment on lines +157 to +166
do {
reportId = try await reportUseCase.report(
title: name,
content: content,
category: category,
location: location,
photos: selectedPhotos)
} catch {
reportId = nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

에러 발생 시 사용자에게 피드백이 없습니다

catch 블록에서 에러를 조용히 무시하고 reportId = nil만 설정합니다. 사용자는 등록이 실패한 이유(네트워크 오류, 서버 오류 등)를 알 수 없어 혼란스러울 수 있습니다.

exceptionSubject를 통해 에러 메시지를 전달하거나, 별도의 에러 상태를 방출하여 UI에서 적절한 피드백을 제공하는 것이 좋습니다.

 do {
     reportId = try await reportUseCase.report(
         title: name,
         content: content,
         category: category,
         location: location,
         photos: selectedPhotos)
 } catch {
+    await MainActor.run {
+        exceptionSubject.send("제보 등록 중 오류가 발생했습니다. 다시 시도해주세요.")
+    }
     reportId = nil
 }
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift
around lines 157-166, the catch block swallows errors and only sets reportId =
nil; instead capture the caught error and emit a user-facing error (e.g., send a
localized message or error object through exceptionSubject or set an error state
property) so the UI can show feedback; update the catch to map the error to a
user-friendly message (or pass the raw error if appropriate) and publish it via
exceptionSubject (or the view model’s error publisher), and keep setting
reportId = nil if needed.

@choijungp choijungp merged commit 0103cc5 into develop Dec 7, 2025
2 checks passed
@choijungp choijungp deleted the feat/report-registration-completion branch December 7, 2025 04:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants